<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Asciidoctor 2.0.20">
<title>使用Angular构建动态可拓展(热插拔)架构的企业级应用</title>
<style>
/* Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */

@import url(//fonts.googleapis.com/css?family=Noto+Sans);
@import url(https://cdn.jsdelivr.net/gh/asciidoctor/asciidoctor@2.0/data/stylesheets/asciidoctor-default.css); /* Default asciidoc style framework - important */

/* CUSTOMISATIONS */
/* Change the values in root for quick customisation. If you want even more fine grain... venture further. */
:root{
--maincolor:#282c34;
--primarycolor:#f39c12;
--secondarycolor:#03a9f4;
--tertiarycolor:#4db6ac;
--sidebarbackground:#21252b;
--linkcolor:#f44336;
--linkcoloralternate:#ff9800;
--white:#FFFFFF;
}

/* Text styles */

body{font-family: "Noto Sans",sans-serif;background-color: var(--maincolor);color:var(--white);}

h1{color:var(--primarycolor) !important;font-family:"Noto Sans",sans-serif;}
h2,h3,h4,h5,h6{color:var(--secondarycolor) !important;font-family:"Noto Sans",sans-serif;}
.title{color:var(--white) !important;font-family:"Noto Sans",sans-serif;font-style: normal; font-weight: normal;}
p{font-family: "Noto Sans",sans-serif ! important}
#toc.toc2 a:link{color:var(--linkcolor);}
#toc.toc2 a:visited{color:var(--linkcolor);}
blockquote{color:var(--tertiarycolor) !important}
.quoteblock{color:var(--white)}
code{color:var(--linkcoloralternate);background-color: var(--sidebarbackground) !important}


/* Table styles */
th{background-color: var(--maincolor);color:var(--white) !important;}
td{background-color: var(--maincolor);color: var(--linkcoloralternate) !important}


#toc.toc2{background-color:var(--sidebarbackground);}
#toctitle{color:var(--white);}

/* Responsiveness fixes */
video {
  max-width: 100%;
}

@media all and (max-width: 600px) {
  table {
    width: 55vw!important;
    font-size: 3vw;
  }
}

.exampleblock > .content {
  background-color: var(--maincolor);
}

a {
  color: var(--secondarycolor);
}

.admonitionblock td.icon .icon-tip::before {
  text-shadow: none;
  color: var(--white);
}
.admonitionblock td.icon .icon-note::before {
  color: var(--tertiarycolor);
}
.admonitionblock td.icon .icon-important::before {
  color: var(--linkcolor);
}
/*.admonitionblock td.icon .icon-caution::before {
  color: var(--linkcoloralternate);
}*/
.admonitionblock td.icon .icon-warning::before {
  color: var(--primarycolor);
}

#preamble > .sectionbody > .paragraph:first-of-type p {
  color: var(--white);
}

.quoteblock blockquote::before {
  color: var(--primarycolor);
}
.quoteblock .attribution cite, .verseblock .attribution cite {
  color: var(--white);
}
.verseblock pre {
  color: var(--white);
}
.quoteblock blockquote, .quoteblock blockquote p {
  color: var(--white);
}

.sidebarblock {
  background: var(--sidebarbackground);
}
.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint {
  background: var(--sidebarbackground);
  color: var(--white);
}

#header .details {
  color: var(--white);
}
#header .details span.email a {
  color: var(--linkcoloralternate);
}
.literalblock pre, .listingblock>.content>pre:not(.highlight), .listingblock>.content>pre[class=highlight], .listingblock>.content>pre[class^="highlight "] {
  background: transparent;
}

</style>
</head>
<body class="article toc2 toc-left">
<div id="header">
<h1>使用Angular构建动态可拓展(热插拔)架构的企业级应用</h1>
<div id="toc" class="toc2">
<div id="toctitle">目录</div>
<ul class="sectlevel1">
<li><a href="#_why_angular">1. 为什么选择 Angular？</a></li>
<li><a href="#_the_goal">2. 目的</a></li>
<li><a href="#_what_is_a_plugin">3. 什么是插件？</a></li>
<li><a href="#_why_is_it_so_hard_to_create_a_plugin_with_angular">4. 为什么用Angular实现插件功能这么难？</a></li>
<li><a href="#_requirements">5. 要求</a></li>
<li><a href="#_similar_solutions">6. 类似的解决方案</a></li>
<li><a href="#_towards_a_solution">7. 解决步骤</a></li>
<li><a href="#_single_bundle">8. 单独的bundle文件</a></li>
<li><a href="#_externals">9. 外部组件</a></li>
<li><a href="#_dynamic_exports">10. 动态导出</a></li>
<li><a href="#_build_plugin">11. 构建插件</a></li>
<li><a href="#_externals_and_shared_angular_libraries">12. 外部以及共享Angular库</a></li>
<li><a href="#_consuming_the_plugin">13. 使用插件</a>
<ul class="sectlevel2">
<li><a href="#_客户端">13.1. 客户端</a></li>
<li><a href="#_服务端">13.2. 服务端</a>
<ul class="sectlevel3">
<li><a href="#_当主应用程序与插件使用相同共享组件的情况">13.2.1. 当主应用程序与插件使用相同共享组件的情况</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#_how_do_we_render_the_plugin">14. 我们如何渲染插件？</a></li>
<li><a href="#_isolating_the_main_app_from_errors_in_a_plugin">15. 将主应用程序与插件中的错误隔离开来</a></li>
<li><a href="#_总结">16. 总结</a></li>
</ul>
</div>
</div>
<div id="content">
<div id="preamble">
<div class="sectionbody">
<div class="paragraph">
<p><a href="https://indepth.dev/posts/1263/building-an-extensible-dynamic-pluggable-enterprise-application-with-angular" target="_target">原文地址</a></p>
</div>
<div class="paragraph">
<p>在这篇文章中，我们会谈谈如何使用Angular CLI构建工具创建AOT预编译的Angular插件，它可以与其它插件共享代码，甚至可以与Angular universal一起使用。 虽然它是一个非官方的解决方案，但对我们的需求场景十分有用。</p>
</div>
<div class="paragraph">
<p>这是我们即将构建的简单示意：</p>
</div>
<div class="paragraph">
<p><span class="image"><img src="assets/preview.gif" alt="preview"></span></p>
</div>
<div class="paragraph">
<p>这是一个简单的页面，我们从一个 <code>plugins-config.json</code> 文件中获取插件的配置。然后，我们首先延迟加载 AOT 编译的插件（<code>plugin1.js</code>），它依赖于 <code>shared.js</code>。shared 库包含一个 Tabs component 和 Tabs ng factories。稍后，我们加载第二个插件（<code>plugin2.js</code>），它重用了之前加载的 <code>shared.js</code> 库中的代码。</p>
</div>
<div class="paragraph">
<p>如果你想查看源代码，这里是 <a href="https://github.com/alexzuza/angular-plugin-architecture" target="_target">Github仓库的链接</a>。</p>
</div>
<div class="paragraph">
<p><code>在示例中我们使用 Angular CLI 7.3.6</code></p>
</div>
<h2 id="_本文讲的是什么" class="discrete">本文讲的是什么？</h2>
<div class="ulist">
<ul>
<li>
<p><a href="#_why_angular">为什么选择 Angular？</a></p>
</li>
<li>
<p><a href="#_the_goal">目的</a></p>
</li>
<li>
<p><a href="#_what_is_a_plugin">什么是插件？</a></p>
</li>
<li>
<p><a href="#_why_is_it_so_hard_to_create_a_plugin_with_angular">为什么用Angular实现插件功能这么难？</a></p>
</li>
<li>
<p><a href="#_requirements">要求</a></p>
</li>
<li>
<p><a href="#_similar_solutions">其它的解决方案</a></p>
</li>
<li>
<p><a href="#_towards_a_solution">逐步解决</a></p>
</li>
<li>
<p><a href="#_single_bundle">单独的bundle文件</a></p>
</li>
<li>
<p><a href="#_externals">外部组件</a></p>
</li>
<li>
<p><a href="#_dynamic_exports">动态导出</a></p>
</li>
<li>
<p><a href="#_build_plugin">构建插件</a></p>
</li>
<li>
<p><a href="#_externals_and_shared_angular_libraries">外部以及共享Angular库</a></p>
</li>
<li>
<p><a href="#_consuming_the_plugin">加载插件(客户端, 服务端)</a></p>
</li>
<li>
<p><a href="#_how_do_we_render_the_plugin">我们如何渲染插件？</a></p>
</li>
<li>
<p><a href="#_isolating_the_main_app_from_errors_in_a_plugin">如何将主应用与插件中的错误隔离开来？</a></p>
</li>
</ul>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_why_angular">1. 为什么选择 Angular？</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Angular 的生态系统很健康，团队和社区一直在飞速发展。</p>
</div>
<div class="paragraph">
<p>Angular 可以帮我们以一种一致性的方式结构化我们的代码，这样每个新的开发者都能轻松的参与到项目中来。</p>
</div>
<div class="paragraph">
<p>我们十分开心 Angular 遵循了最佳web实践，想一下typescript、rxjs、服务端渲染、web workers、differential loading、progressive web application(PWA)、懒加载等等特性。 这些都能帮我们快速用上这些功能。</p>
</div>
<div class="paragraph">
<p>Angular还未我们提供了其它一些功能，比如内置依赖注入系统、响应式表单、schematics等等。</p>
</div>
<div class="paragraph">
<p>这就是为什么我们在构建企业级应用的时候经常使用Angular的原因。</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_the_goal">2. 目的</h2>
<div class="sectionbody">
<div class="paragraph">
<p>有一天，我们的客户要求我们为他现有的Angular Universal应用添加一个功能。 他想要实现一个可插拔的内容管理系统(CMS)。 我们的目的是为当前应用添加一个可拓展的功能，这样第三方开发者就可以轻松地开发自己的模块并将之上传。 然后，Angular应用会感知并将它集成到架构中，而不必编译整个应用并重新部署。</p>
</div>
<div class="paragraph">
<p>简单地说，我们的目的是开发一个<strong>插件系统</strong>。</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_what_is_a_plugin">3. 什么是插件？</h2>
<div class="sectionbody">
<div class="paragraph">
<p>插件系统可以在不修改核心应用源代码的情况下拓展应用。</p>
</div>
<div class="paragraph">
<p>这个需求听起来很简单，但用Angular实现插件功能一直是一个挑战。</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_why_is_it_so_hard_to_create_a_plugin_with_angular">4. 为什么用Angular实现插件功能这么难？</h2>
<div class="sectionbody">
<div class="paragraph">
<p>我的一个同事在很久之前就开始使用AngularJS工作了，他说他曾经看到过一个用纯es5编写的Angular2应用(嗯。。。也许他看到的是我写的 <a href="https://jsfiddle.net/yurzui/1hk7knwq/" target="_target">jsfiddle</a>，或者是一个我在更久远的时候写的 <a href="https://github.com/alexzuza/angular2-build-examples/tree/master/es5" target="_target">repo</a>)。 因此，他提议在es5中创建一个Angular模块，把它放在一个文件夹里，然后让主应用以某种方式让它跑起来。</p>
</div>
<div class="paragraph">
<p><code>不要误解我的意思了，我从Angular2的alpha版本就开始使用Angular了，Angular2(或者简单的说Angular)是一个全新的东西。</code></p>
</div>
<div class="paragraph">
<p>当然，现在主要的困难是Ahead Of Time (AOT)编译。 使用AOT最主要的优点是应用的性能更好。</p>
</div>
<div class="paragraph">
<p>我看到过许多使用JitCompiler构建的可插拔架构示例。 这些都不完全符合我的需求。 我们必须保证应用的性能，而不是简单粗暴地在主应用中嵌入一段编译器代码。 这就是为什么我们不应该使用es5，因为只有TypeScript可以使用AOT预编译。 然而，当前AngularAOT的实现是基于 <strong>@NgModule</strong> 传递scope的，并且要求所有代码都要放在一起编译。</p>
</div>
<div class="paragraph">
<p>另一个困难是，为了避免重复编写代码，我们需要在插件之间共享代码。 那么什么样的重复代码才是我们可以提取的呢？ 我区分了两种可以重复使用的代码：</p>
</div>
<div class="ulist">
<ul>
<li>
<p>我们编写的或者是从 <strong>node_modules</strong> 中拉取的代码</p>
</li>
<li>
<p>AOT生成的代码，例如组件以及模块factories (<strong>component.ngfactory.js</strong> 和 <strong>module.ngfactory.js</strong>)。 实际上，这部分代码的数量十分巨大。</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>为了避免这些代码重复，我们需要解决 ViewEngine 如何生成工厂的问题。</p>
</div>
<div class="paragraph">
<p><code>以防您不了解，ViewEngine 是 Angular 目前使用的渲染引擎。</code></p>
</div>
<div class="paragraph">
<p>这里的问题在于 Angular 编译器生成的代码可以指向来自另一段生成的代码的 ViewFactory。例如，这里是如何定义element与 ViewDefinitionFactory 相关联的示例（ <a href="https://github.com/angular/angular/blob/f8096d499324cf0961f092944bbaedd05364eea1/packages/core/src/view/element.ts#L64-L65" target="_target">GitHub 源代码</a>）：</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-typescript" data-lang="typescript">elementDef(…, componentView?: null | ViewDefinitionFactory, componentRendererType?: RendererType2 | null)</code></pre>
</div>
</div>
<div class="paragraph">
<p>最终可以从shared库中的factories获取重复代码。</p>
</div>
<div class="paragraph">
<div class="title">未优化插件的ngFactories是重复的</div>
<p><span class="image"><img src="assets/image-27.png" alt="image 27"></span></p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_requirements">5. 要求</h2>
<div class="sectionbody">
<div class="paragraph">
<p>所以在我们讨论Angular插件系统的时候，我们应该遵循这些内容：</p>
</div>
<div class="ulist">
<ul>
<li>
<p>AOT</p>
</li>
<li>
<p>避免重复编写代码 (类似这些包 <code>@angular/core{common,forms,router},rxjs,tslib</code>)</p>
</li>
<li>
<p>在所有插件中使用共享库，但不要在每个插件中复制一份共享库的factories，而是应该重用库中的代码以及factories。</p>
</li>
<li>
<p>对于导入外部模块，我们只需要知道一件事：它们的bundle文件路径。</p>
</li>
<li>
<p>我们的代码应该识别模块并将插件嵌入到页面中。</p>
</li>
<li>
<p>支持服务器端渲染</p>
</li>
<li>
<p>只在需要时加载module</p>
</li>
<li>
<p>支持与 Angular CLI 提供给我们的相同等级的优化</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>所有这些因素共同决定了我们的解决方案。</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_similar_solutions">6. 类似的解决方案</h2>
<div class="sectionbody">
<div class="paragraph">
<p>有不同的 <a href="https://stackoverflow.com/questions/41438198/implementing-a-plugin-architecture-plugin-system-pluggable-framework-in-angu">方案</a> 可供选择，但它们通常缺少一些关键部分：对AOT的支持、优化代码以及提取重复代码。</p>
</div>
<div class="paragraph">
<p>其中一个接近我们需求的解决方案是： <a href="https://github.com/iwnow/angular-plugin-example" class="bare">https://github.com/iwnow/angular-plugin-example</a></p>
</div>
<div class="paragraph">
<p>它使用Rollup以umd格式生成插件捆绑包。</p>
</div>
<div class="paragraph">
<p>然而，我从这种方案中发现了以下缺点：</p>
</div>
<div class="ulist">
<ul>
<li>
<p>❌ 它不使用Angular CLI提供给我们的优化技术，即不会去除Angular的注解(decorators)以及不会运行buildOptimizer。</p>
</li>
<li>
<p>❌ 如果我们在每个插件中使用共享组件，它会导致工厂的重复。</p>
</li>
</ul>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_towards_a_solution">7. 解决步骤</h2>
<div class="sectionbody">
<div class="paragraph">
<p>幸运的是，Angular可以通过自定义脚本实现可扩展性。</p>
</div>
<div class="paragraph">
<p>从Angular 6开始，我们可以使用构建器来介入编译过程。 它允许我们添加自定义的webpack配置，包括从原始webpack设置中可以想象到的所有好处。</p>
</div>
<div class="paragraph">
<p>因此我想到一个不错的注意，就是编写一个自定义构建器来构建插件。</p>
</div>
<div class="paragraph">
<p>Angular CLI支持生成库。 但是作为构建库的工具，ng-packagr生成了许多我们不需要的文件。 (是的，我知道它遵循Angular包格式(APF))。 而这些文件只能被其他Angular应用程序所使用。</p>
</div>
<div class="paragraph">
<p>我觉得可以使用一个Angular应用程序来构建我的插件。 在这我指的是使用类似<strong>ng generate application</strong>这样的CLI命令生成的应用。 这种方法有很多好处，因为它为我们提供了与Angular CLI相同等级的优化功能。 例如，在AOT之后，我们不会得到仅用于Angular的decorators。</p>
</div>
<div class="paragraph">
<div class="title">未优化bundle中的Decorators</div>
<p><span class="image"><img src="assets/image-28.png" alt="image 28"></span></p>
</div>
<div class="paragraph">
<p>现在，我只能想到一种方案，可以将所有需要的代码打成一个单独的bundle文件了：</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_single_bundle">8. 单独的bundle文件</h2>
<div class="sectionbody">
<div class="paragraph">
<p>那么首先我们应该：</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-shell" data-lang="shell">ng generate application plugins —minimal</code></pre>
</div>
</div>
<div class="paragraph">
<p>这时我们会得到一个简单的Angular应用，它包含在 <strong>projects</strong> 文件夹中</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code>projects
  |_ plugins
src
angular.json</code></pre>
</div>
</div>
<div class="paragraph">
<p>接下来，我删除了所有用不到的文件，此时的文件结构是这样：</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code>projects
   |_ plugins
        |_ src
            |_ plugin1
            |_ plugin2
            main.ts
        tsconfig.app.json
src
angular.json</code></pre>
</div>
</div>
<div class="paragraph">
<p>然后我从项目中的 <strong>angular.json</strong> 文件里删除了许多多余的东西：</p>
</div>
<div class="paragraph">
<div class="title">angular.json中plugins的配置</div>
<p><span class="image"><img src="assets/image-29.png" alt="image 29"></span></p>
</div>
<div class="paragraph">
<p>如你所见，我删除了 <strong>index</strong>、所有 <strong>assets</strong>、<strong>scripts</strong>、<strong>fileReplacements</strong> 等等。我还创建了一个位于 <strong>./builders:plugin</strong> 中的自定义 <a href="https://github.com/alexzuza/angular-plugin-architecture/blob/58b20d182858ed9ff15585c3cbcc931effeae577/builders/plugin-builder/index.ts#L28">PluginBuilder</a>。</p>
</div>
<div class="paragraph">
<p>这个构建器应该生成一个<strong>单独的 UMD bundle文件</strong>：</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-typescript" data-lang="typescript">// 确认我们生成的是单独的bundle文件
delete config.entry.polyfills;
delete config.optimization.runtimeChunk;
delete config.optimization.splitChunks;
delete config.entry.styles;
config.output.library = pluginName;
config.output.libraryTarget = 'umd';</code></pre>
</div>
</div>
<div class="paragraph">
<p>此外，它应该提供 <strong>provide externals</strong> 并根据正在构建的插件<strong>生成适当的导出</strong>。这是构建器的一个至关重要的功能。</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_externals">9. 外部组件</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Webpack允许我们提供外部组件。所以它们看起来是这样的:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-javascript" data-lang="javascript">config.externals = {
  rxjs: 'rxjs',
  '@angular/core': 'ng.core',
  '@angular/common': 'ng.common',
  '@angular/forms': 'ng.forms',
  '@angular/router': 'ng.router',
  tslib: 'tslib'
  // 把其他常见的依赖放在这里
};</code></pre>
</div>
</div>
<div class="paragraph">
<p>还有另一个可选项，稍后我将在 <a href="#_externals_and_shared_angular_libraries"><strong>Shared library</strong></a> 一节中展示。</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_dynamic_exports">10. 动态导出</h2>
<div class="sectionbody">
<div class="paragraph">
<p>一个有趣的部分是我们可以使用相同的构建命令构建不同的插件。</p>
</div>
<div class="paragraph">
<p>首先，<strong>plugins</strong>应用的 <strong>tsconfig.app.json</strong> 只关注 <strong>main.ts</strong> 文件，这样我们就只需要编译 <strong>main.ts</strong> 中提供的内容。</p>
</div>
<div class="listingblock">
<div class="title">tsconfig.app.json</div>
<div class="content">
<pre class="highlight"><code class="language-json" data-lang="json">{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    ...
  },
  "files": ["./src/main.ts"]
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>接下来，为构建器提供<strong>modulePath</strong>参数:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code>modulePath=./plugin1/plugin1.module#Plugin1Module</code></pre>
</div>
</div>
<div class="paragraph">
<p>你可能会注意到，这与懒加载时编写的需要延迟加载的模块路径很相似。构建器会分析这个路径，并在 <strong>main.ts</strong> 中生成专门的代码。例如，在上面的 <strong>modulePath</strong> 中，我们会得到：</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-typescript" data-lang="typescript">export * from './plugin1/plugin1.module';
export * from './plugin1/plugin1.module.ngfactory';
import { Plugin1ModuleNgFactory } from './plugin1/plugin1.module.ngfactory';
export default Plugin1ModuleNgFactory;</code></pre>
</div>
</div>
<div class="paragraph">
<p>这里的关键点是不仅仅导出代码，还应该包括由Angular编译器生成的factories。</p>
</div>
<div class="paragraph">
<p><strong>main.ts</strong> 为空，这样在构建之前我可以动态更新它，这样它将导出我们想要的内容。</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_build_plugin">11. 构建插件</h2>
<div class="sectionbody">
<div class="paragraph">
<p>下面的示例是我用来构建插件的命令:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-json" data-lang="json">"build:plugin1": "ng build --project plugins --prod --modulePath=./plugin1/plugin1.module#Plugin1Module --pluginName=plugin1 --outputPath=./src/assets/plugins",</code></pre>
</div>
</div>
<div class="paragraph">
<p>这一步干了什么？</p>
</div>
<div class="ulist">
<ul>
<li>
<p><strong>ng build — project plugins — prod</strong><br>
在prod模式对上面创建的plugins应用进行构建。</p>
</li>
<li>
<p><strong>modulePath=./plugin1/plugin1.module#Plugin1Module</strong><br>
提供插件的模块路径。</p>
</li>
<li>
<p><strong>pluginName=plugin1</strong><br>
定义插件的bundle名称</p>
</li>
<li>
<p><strong>— outputPath=./src/assets/plugins</strong><br>
指定bundle文件的存放目录。</p>
</li>
</ul>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_externals_and_shared_angular_libraries">12. 外部以及共享Angular库</h2>
<div class="sectionbody">
<div class="paragraph">
<p>我们也可以在插件中使用共享库。为了能够从共享库中暴露所有工厂，我们需要将库代码放在一个文件中。否则，很难弄清楚那些ngfactories的路径是什么。</p>
</div>
<div class="paragraph">
<p>这就是ng-packagr为我们生成的东西。</p>
</div>
<div class="paragraph">
<p>我们可以由ng-packagr文件构建生成。所以首先，我创建一个库：</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-shell" data-lang="shell">ng generate library shared</code></pre>
</div>
</div>
<div class="paragraph">
<p>然后构建它一边让插件系统使用：</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-json" data-lang="json">"build:shared": "ng build shared &amp;&amp; ng build --project plugins --prod --modulePath=shared#SharedModule --pluginName=shared  --outputPath=./src/assets/plugins",</code></pre>
</div>
</div>
<div class="paragraph">
<p>现在，当我创建一个 <strong>shared</strong> 库时，我就可以在我的插件中重复使用它：</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-json" data-lang="json">"build:plugin1": "ng build --project plugins --prod --modulePath=./plugin1/plugin1.module#Plugin1Module --pluginName=plugin1 --sharedLibs=shared --outputPath=./src/assets/plugins",</code></pre>
</div>
</div>
<div class="paragraph">
<p>注意 <strong>--sharedLibs=shared</strong></p>
</div>
<div class="paragraph">
<p>我通过Webpack的 <strong>externals</strong> 选项来控制factory外部依赖，其中我提供了一个 <a href="https://webpack.js.org/configuration/externals/#function" target="_target">特殊函数</a> 来捕获所有 <strong>ngfactory</strong> import。</p>
</div>
<div class="paragraph">
<div class="title">Webpack对外部组件捕获factories imports</div>
<p><span class="image"><img src="assets/image-30.png" alt="image 30"></span></p>
</div>
<div class="paragraph">
<p>这个方法我们在我们的bundle中覆盖了外部所有可能的东西：</p>
</div>
<div class="paragraph">
<div class="title">Webpack外部组件涵盖了所有可能的外部imports</div>
<p><span class="image"><img src="assets/image-31.png" alt="image 31"></span></p>
</div>
<div class="paragraph">
<p>最后就得到了我们想要的效果：</p>
</div>
<div class="paragraph">
<p><span class="image"><img src="assets/image-32.png" alt="image 32"></span></p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_consuming_the_plugin">13. 使用插件</h2>
<div class="sectionbody">
<div class="paragraph">
<p>首先，我们需要从服务器加载我们的配置。</p>
</div>
<div class="paragraph">
<p>为了简单起见，我们加载一个简单的JSON文件，其内容如下：</p>
</div>
<div class="paragraph">
<div class="title">plugins-config.json</div>
<p><span class="image"><img src="assets/image-33.png" alt="image 33"></span></p>
</div>
<div class="paragraph">
<p>然后，为了使用构建的插件，我们创建了两个服务，一个用于客户端，一个用于服务器端，它们都继承了 <strong>PluginLoaderService</strong> 抽象类：</p>
</div>
<div class="paragraph">
<div class="title">LoaderService的基本接口</div>
<p><span class="image"><img src="assets/image-34.png" alt="image 34"></span></p>
</div>
<div class="sect2">
<h3 id="_客户端">13.1. 客户端</h3>
<div class="paragraph">
<p>我们使用Angular的依赖注入来为客户端代码提供仅适用于客户端的加载器：</p>
</div>
<div class="listingblock">
<div class="title">app.module.ts</div>
<div class="content">
<pre class="highlight"><code class="language-typescript" data-lang="typescript">providers: [
  { provide: PluginLoaderService, useClass: ClientPluginLoaderService }
],</code></pre>
</div>
</div>
<div class="paragraph">
<p>为了在客户端加载插件，我们使用一个支持加载AMD模块的最小化的 <strong>systemjs@3.0.2</strong> 构建。</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code>"scripts": [
  "node_modules/systemjs/dist/s.js",
  "node_modules/systemjs/dist/extras/named-register.js",
  "node_modules/systemjs/dist/extras/amd.js"
],</code></pre>
</div>
</div>
<div class="paragraph">
<p>外部依赖是由全局函数 <strong>define</strong> 提供的：</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code>Object.keys(PLUGIN_EXTERNALS_MAP).forEach(externalKey =&gt;
  window.define(externalKey, [], () =&gt; PLUGIN_EXTERNALS_MAP[externalKey])
);</code></pre>
</div>
</div>
<div class="paragraph">
<p>然后加载函数如下</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-typescript" data-lang="typescript">load&lt;T&gt;(pluginName): Promise&lt;NgModuleFactory&lt;T&gt;&gt; {
  return SystemJs.import(config[pluginName].path).then(
    module =&gt; module.default.default
  );
}</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_服务端">13.2. 服务端</h3>
<div class="paragraph">
<p>服务器端的代码实现自己的 loader service：</p>
</div>
<div class="listingblock">
<div class="title">app.server.module.ts</div>
<div class="content">
<pre class="highlight"><code class="language-typescript" data-lang="typescript">providers: [
  { provide: PluginLoaderService, useClass: ServerPluginLoaderService }
],</code></pre>
</div>
</div>
<div class="paragraph">
<p>该服务使用 <a href="https://nodejs.org/api/modules.html#modules_require_id">nodejs</a> 的require方法来获取模块：</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-typescript" data-lang="typescript">load&lt;T&gt;(pluginName): Promise&lt;NgModuleFactory&lt;T&gt;&gt; {
  const factory = global['require'](`./browser${config[pluginName].path}`)
    .default;
  return Promise.resolve(factory);
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>我们还覆盖了原生的 <strong>require</strong> 方法，目的是提供外部依赖：</p>
</div>
<div class="paragraph">
<div class="title">服务端的外部依赖Externals</div>
<p><span class="image"><img src="assets/image-35.png" alt="image 35"></span></p>
</div>
<div class="sect3">
<h4 id="_当主应用程序与插件使用相同共享组件的情况">13.2.1. 当主应用程序与插件使用相同共享组件的情况</h4>
<div class="paragraph">
<p>现在让我们来考虑一个场景，我们的共享库也应该在我们的主应用程序中使用：</p>
</div>
<div class="paragraph">
<div class="title">插件和主应用程序共享相同的库</div>
<p><span class="image"><img src="assets/image-36.png" alt="image 36"></span></p>
</div>
<div class="paragraph">
<p>我们该如何避免代码重复？</p>
</div>
<div class="paragraph">
<p>既然我们在主应用程序中已经构建了 <strong>shared</strong> 库，我们可以将其定义为其他插件的外部依赖项。</p>
</div>
<div class="listingblock">
<div class="title">plugin-externals.prod.ts</div>
<div class="content">
<pre class="highlight"><code class="language-typescript" data-lang="typescript">import * as shared from 'shared';
export const PLUGIN_EXTERNALS_MAP = {
  'ng.core': core,
  'ng.common': common,
  'ng.forms': forms,
  'ng.router': router,
  rxjs,
  tslib,
  shared: { ...shared, ...require('shared/shared.ngfactory') }
};</code></pre>
</div>
</div>
<div class="paragraph">
<p>这样我们就可以同时共享库代码以及由AOT产生的代码了。</p>
</div>
<div class="paragraph">
<p>你可能会注意到我在上面的文件中使用了prod后缀。那是因为我们定义这样的外部依赖是只为生产环境构建的。</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-json" data-lang="json">"configurations": {
  "production": {
    "fileReplacements": [
      ...
      {
        "replace": "src/app/services/plugin-loader/plugin-externals.ts",
        "with": "src/app/services/plugin-loader/plugin-externals.prod.ts"
      }
    ],</code></pre>
</div>
</div>
<div class="paragraph">
<p>在开发模式下，我们仍然通过Systemjs加载 <strong>shared</strong> 库作为其它插件。</p>
</div>
<div class="paragraph">
<p>该场景的示例代码可以在这里查看 <a href="https://github.com/alexzuza/angular-plugin-architecture/tree/share-lib-between-app-and-plugins">share-lib-between-app-and-plugins</a>。</p>
</div>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_how_do_we_render_the_plugin">14. 我们如何渲染插件？</h2>
<div class="sectionbody">
<div class="paragraph">
<p>既然我们已经获得了获取NgModuleFactory所需的所有内容，那么我们就可以借助Angular中用于创建动态组件的大家都知道的API方法来渲染我们的插件了：</p>
</div>
<div class="ulist">
<ul>
<li>
<p><strong>componentFactory.create</strong></p>
</li>
<li>
<p><strong>viewContainerRef.createComponent</strong></p>
</li>
</ul>
</div>
<div class="paragraph">
<p>在下面的示例中，我们采用了第二种方案：</p>
</div>
<div class="paragraph">
<div class="title">用于渲染插件的底层API</div>
<p><span class="image"><img src="assets/image-37.png" alt="image 37"></span></p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_isolating_the_main_app_from_errors_in_a_plugin">15. 将主应用程序与插件中的错误隔离开来</h2>
<div class="sectionbody">
<div class="paragraph">
<p>可插拔架构的一个问题是插件中的异常可能会导致整个应用程序崩溃。</p>
</div>
<div class="paragraph">
<p>幸运的是，有几种不同的方法可以处理这种错误：</p>
</div>
<div class="ulist">
<ul>
<li>
<p>全局 ErrorHandler</p>
</li>
<li>
<p>Zone.onError.subscribe</p>
</li>
<li>
<p>try catch</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>这些方法让我们可以优雅地处理创建插件时出现的错误，并安全地隔离、终止以及报告有问题的组件，而不会影响到应用程序的其他部分。</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_总结">16. 总结</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Angular 每个季度都在不断发展。但是，它仍然没有一种简单的方法来处理动态模板。当我们使用大型应用程序并且客户要求所有内容都应该在运行时动态、懒加载以及可变更时，这是最需要的一部分。</p>
</div>
<div class="paragraph">
<p>幸运的是，Angular CLI 为我们提供了配置特定需求的构建的工具。本文介绍了一种如何构建 AOT 预编译插件的方案。它可能并不完全适用于您的需求，但您可能会从本文中的一些想法中受益。因此，我希望您能从中发现有用的部分。</p>
</div>
<div class="paragraph">
<p>感谢阅读！</p>
</div>
<div class="paragraph">
<p>代码可以在Github上找到： <a href="https://github.com/alexzuza/angular-plugin-architecture">angular-plugin-architecture</a></p>
</div>
</div>
</div>
</div>
</body>
</html>